// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import path from 'node:path';

import { createFsFromVolume, Volume, type IFs } from 'memfs';
import type {
  StatsCompilation as WebpackStatsCompilation,
  MultiStats,
  Stats,
  Configuration,
  Compiler,
  StatsError,
  OutputFileSystem
} from 'webpack';
import webpackMerge from 'webpack-merge';

/**
 * @public
 * This function generates a webpack compiler with default configuration and the output filesystem mapped to
 * a memory filesystem. This is useful for testing webpack plugins/loaders where we do not need to write to disk (which can be costly).
 * @param entry - The entry point for the webpack compiler
 * @param additionalConfig - Any additional configuration that should be merged with the default configuration
 * @param memFs - The memory filesystem to use for the output filesystem. Use this option if you want to _inspect_, analyze, or read the output
 * files generated by the webpack compiler. If you do not need to do this, you can omit this parameter and the output files.
 *
 * @returns - A webpack compiler with the output filesystem mapped to a memory filesystem
 *
 * @example
 * ```typescript
 * import Testing from '@rushstack/webpack-plugin-utilities';
 *
 * describe('MyPlugin', () => {
 *   it('should run', async () => {
 *     const stats = await Testing.getTestingWebpackCompiler(
 *       `./src/index.ts`,
 *     );
 *
 *     expect(stats).toBeDefined();
 *  });
 * });
 * ```
 *
 * @remarks
 * If you want to be able to read, analyze, access the files written to the memory filesystem,
 * you can pass in a memory filesystem instance to the `memFs` parameter.
 *
 * @example
 * ```typescript
 * import Testing from '@rushstack/webpack-plugin-utilities';
 * import { createFsFromVolume, Volume, IFs } from 'memfs';
 * import path from 'path';
 *
 * describe('MyPlugin', () => {
 *  it('should run', async () => {
 *    const virtualFileSystem: IFs = createFsFromVolume(new Volume());
 *    const stats = await Testing.getTestingWebpackCompiler(
 *      `./src/index.ts`,
 *      {},
 *      virtualFileSystem
 *    );
 *
 *    expect(stats).toBeDefined();
 *    expect(virtualFileSystem.existsSync(path.join(__dirname, 'dist', 'index.js'))).toBe(true);
 *  });
 * });
 * ```
 */
export async function getTestingWebpackCompilerAsync(
  entry: string,
  additionalConfig: Configuration = {},
  memFs: IFs = createFsFromVolume(new Volume())
): Promise<(Stats | MultiStats) | undefined> {
  let webpackModule: typeof import('webpack');
  try {
    webpackModule = (await import('webpack')).default;
  } catch (e) {
    throw new Error(
      'Unable to load module "webpack". The @rushstack/webpack-plugin-utilities package declares "webpack" as ' +
        'an optional peer dependency, but a function was invoked on it that requires webpack. Make sure ' +
        `the peer dependency on "webpack" is fulfilled. Inner error: ${e}`
    );
  }

  const compilerOptions: Configuration = webpackMerge(_defaultWebpackConfig(entry), additionalConfig);
  const compiler: Compiler = webpackModule(compilerOptions);

  // The memFs Volume satisfies the interface contract, but the types aren't happy due to strict null checks
  const outputFileSystem: OutputFileSystem = memFs as unknown as OutputFileSystem;
  outputFileSystem.join = path.join.bind(path);

  compiler.outputFileSystem = outputFileSystem;

  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      compiler.close(() => {
        if (err) {
          return reject(err);
        }

        _processAndHandleStatsErrorsAndWarnings(stats, reject);

        resolve(stats);
      });
    });
  });
}

function _processAndHandleStatsErrorsAndWarnings(
  stats: Stats | MultiStats | undefined,
  reject: (reason: unknown) => void
): void {
  if (stats?.hasErrors() || stats?.hasWarnings()) {
    const serializedStats: WebpackStatsCompilation[] = [stats?.toJson('errors-warnings')];

    const errors: StatsError[] = [];
    const warnings: StatsError[] = [];

    for (const compilationStats of serializedStats) {
      if (compilationStats.warnings) {
        for (const warning of compilationStats.warnings) {
          warnings.push(warning);
        }
      }

      if (compilationStats.errors) {
        for (const error of compilationStats.errors) {
          errors.push(error);
        }
      }

      if (compilationStats.children) {
        for (const child of compilationStats.children) {
          serializedStats.push(child);
        }
      }
    }

    reject([...errors, ...warnings]);
  }
}

function _defaultWebpackConfig(entry: string = './src'): Configuration {
  return {
    // We don't want to have eval source maps, nor minification
    // so we set mode to 'none' to disable both. Default is 'production'
    mode: 'none',
    context: __dirname,
    entry,
    output: {
      filename: 'test-bundle.js'
    }
  };
}
